 /*******************************************************************************
  * Copyright (c) 2004, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.intro.impl.model;

 import java.util.Hashtable ;
 import java.util.Vector ;

 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.help.internal.UAElement;
 import org.eclipse.help.internal.UAElementFactory;
 import org.eclipse.help.internal.dynamic.DocumentProcessor;
 import org.eclipse.help.internal.dynamic.FilterHandler;
 import org.eclipse.help.internal.dynamic.ProcessorHandler;
 import org.eclipse.ui.internal.intro.impl.IIntroConstants;
 import org.eclipse.ui.internal.intro.impl.model.loader.ExtensionPointManager;
 import org.eclipse.ui.internal.intro.impl.model.loader.IntroContentParser;
 import org.eclipse.ui.internal.intro.impl.model.loader.ModelLoaderUtil;
 import org.eclipse.ui.internal.intro.impl.model.util.BundleUtil;
 import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
 import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext;
 import org.eclipse.ui.internal.intro.impl.util.Log;
 import org.eclipse.ui.internal.intro.impl.util.StringUtil;
 import org.osgi.framework.Bundle;
 import org.w3c.dom.Document ;
 import org.w3c.dom.Element ;
 import org.w3c.dom.Node ;
 import org.w3c.dom.NodeList ;

 /**
  * Base class for all Intro pages, inlcuding HomePage. Adds page specific
  * support:
  * <ul>
  * <li>support for page styles, and style inheritance</li>
  * <li>support for XHTML via a DOM instance var. Resolving the page is also
  * handled here.</li>
  * <li>a pge has the concept of being an IFramePage. This is indicated by the
  * isIFrame flag. A page is an IFramePage when it is not defined in any content
  * file, but instead is actually created at runtime. This is done to display a
  * Help System topic embedded in any given div. The current page is cloned, its
  * id is mangled with "_embedDivId", the content of the div pointed to by
  * embedDivId is replaced with an IFrame that loads the Help System topic.</li>
  * </ul>
  */
 public abstract class AbstractIntroPage extends AbstractIntroContainer {

     protected static final String TAG_PAGE = "page"; //$NON-NLS-1$
 private static final String ATT_STYLE = "style"; //$NON-NLS-1$
 private static final String ATT_ALT_STYLE = "alt-style"; //$NON-NLS-1$
 private static final String ATT_CONTENT = "content"; //$NON-NLS-1$
 private static final String ATT_SHARED_STYLE = "shared-style"; //$NON-NLS-1$
 private static final String INVALID_CONTENT = "invalidPage/invalidPage.xhtml"; //$NON-NLS-1$
 private static final String INVALID_CONTENT_BASE = "invalidPage"; //$NON-NLS-1$

     private String style;
     private String altStyle;
     private String sharedStyle;
     private IntroPageTitle title;
     private String content;

     // if iframe is not null, this indicates that this page was cloned at
 // runtime from another page whose id was "originalId".
 private IntroInjectedIFrame iframe;

     // id of page from which this page was cloned.
 private String originalId;

     // DOM representing XHTML content. DOM is only cached in the case of XHTML
 // content.
 private Document dom;
     
     private DocumentProcessor domProcessor;

     // set when the content file is loaded (ie: loadChildren is called)
 private boolean isXHTMLPage;

     // Model base attribute is stored in parent class. This base attribute here
 // is to cache the initial location of the content file. When content
 // attribute is defined, the base in the model becomes relative to
 // new xml file. However, in the case of XHTML content, and when
 // presentation is UI forms, we need to reuse initial content file location.
 private String initialBase;

     /**
      * The vectors to hold all inhertied styles and alt styles from included
      * elements. They are lazily created when children are resolved (ie:
      * includes are resolved) OR when extensions are resolved and styles need to
      * be added to the target page.
      * <p>
      * Style Rules:
      * <ul>
      * <li>For includes, merge-style controls wether or not the enclosing page
      * inherits the styles of the target.
      * <li>If a page is including a shared div, merging target styles into this
      * page is ignored. Shared divs do not have styles.</li>
      * <li>For extensions, if the style or alt-style is not defined, that means
      * that no style inheritence is needed, and the style of the target page are
      * not updated.
      * <li>If an extension is extending a shared div, merging the styles of
      * this extension into the target is ignored. Shared divs do not have
      * styles.</li>
      * <li>Shared hashtable has alt-styles as keys and bundles as values.</li>
      * </ul>
      */
     private Vector styles;
     private Hashtable altStyles;

     /**
      * Parent class for all pages. Make sure to set the bundle to where the
      * pages are loaded from. This means that the bundle for root may be
      * different from the bundle for all the other pages. Only pages do this
      * logic and so other model objects might have the wrong bundle cached if
      * the resource was loaded from an nl directory.
      *
      * @param element
      */
     AbstractIntroPage(Element element, Bundle bundle, String base) {
         super(element, bundle, base);
         this.initialBase = base;
         content = getAttribute(element, ATT_CONTENT);
         if (content == null) {
             //Delaying init until we have the model
 // so that we can resolve theme
 //init(element, bundle, base);
 }
         else {
             // Content is not null. Resolve it. Other page attributes (style,
 // alt-style...) will be loaded when xml content file is loaded
 // since we need to pick them up from external xml content file. In
 // the case where this external content file is XHTML and we have
 // HTML presentation, page attributes are simply not loaded. In the
 // case where we have XHTML in a UI forms presentation, we will need
 // to load initial page attributes.
 // BASE: since content is being loaded from another xml file, point
 // the base of this page to be relative to the new xml file
 // location.
 IPath subBase = ModelUtil.getParentFolderPath(content);
             this.base = new Path(base).append(subBase).toString();
             content = BundleUtil.getResolvedResourceLocation(base, content,
                 bundle);
         }
         // load shared-style attribure. This is needed in the XML and in the
 // XHTML cases. Default is to include shared style.
 this.sharedStyle = getAttribute(element, ATT_SHARED_STYLE);
         if (sharedStyle == null)
             sharedStyle = "true"; //$NON-NLS-1$

     }
     
     public void setParent(AbstractIntroElement parent) {
         super.setParent(parent);
         if (content == null)
             init(element, getBundle(), initialBase);
     }
     
     /**
      * Returns unresolved content value as found in the source file.
      * the source file.
      * @return the unresolved content value
      */
     
     public String getRawContent() {
         return getAttribute(element, ATT_CONTENT);
     }

     /**
      * Initialize styles. Take first style in style attribute and make it the
      * page style. Then put other styles in styles vectors. Make sure to resolve
      * each style.
      *
      * @param element
      * @param bundle
      */
     private void init(Element element, Bundle bundle, String base) {
         String [] styleValues = getAttributeList(element, ATT_STYLE);
         if (styleValues != null && styleValues.length > 0) {
             style = styleValues[0];
             style = BundleUtil.getResolvedResourceLocation(base, style, bundle);
             for (int i = 1; i < styleValues.length; i++) {
                 String style = styleValues[i];
                 style = BundleUtil.getResolvedResourceLocation(base, style,
                     bundle);
                 addStyle(style);
             }
         }

         String [] altStyleValues = getAttributeList(element, ATT_ALT_STYLE);
         if (altStyleValues != null && altStyleValues.length > 0) {
             altStyle = altStyleValues[0];
             altStyle = BundleUtil.getResolvedResourceLocation(base, altStyle,
                 bundle);
             for (int i = 1; i < altStyleValues.length; i++) {
                 String style = altStyleValues[i];
                 style = BundleUtil.getResolvedResourceLocation(base, style,
                     bundle);
                 addAltStyle(style, bundle);
             }
         }
     }




     /**
      * The page's title. Each page can have one title.
      *
      * @return Returns the title of this page.
      */
     public String getTitle() {
         // title is a child of the page, and so we have to load children first.
 // We also have to resolve children because someone might be including a
 // title. Update title instance after all includes and extensions have
 // been resolved.
 getChildren();
         if (title == null) {
             // there should only be one title child per page. safe to cast.
 IntroPageTitle[] titles = (IntroPageTitle[]) getChildrenOfType(AbstractIntroElement.PAGE_TITLE);
             if (titles.length > 0)
                 title = titles[0];
         }

         if (title == null)
             // still null. no title.
 return null;
         return title.getTitle();
     }

     /**
      * @return Returns the style.
      */
     public String getStyle() {
         return style;
     }

     /**
      * @return Returns the alt_style.
      */
     public String getAltStyle() {
         return altStyle;
     }

     /**
      * Gets all the inherited styles of this page. Styles can be inherited from
      * includes or from configExtensions.
      * <p>
      * Note: this call needs to get all the children of this page, and so it
      * will resolve this page. might be expensive.
      *
      * @return Returns all the inherited styles of this page. Returns an empty
      * array if page is not expandable or does not have inherited
      * styles.
      */
     public String [] getStyles() {
         // call get children first to resolve includes and populate styles
 // vector. Resolving children will initialize the style vectors.
 getChildren();
         if (styles == null)
             // style vector is still null because page does not have styles.
 return new String [0];
         String [] stylesArray = new String [styles.size()];
         styles.copyInto(stylesArray);
         return stylesArray;
     }

     /**
      * Gets all the inherited alt-styles of this page (ie: styles for the SWT
      * presentation). A hashtable is returned that has inhertied alt-styles as
      * keys, and plugin descriptors as values. This is needed to be able to load
      * resources from the inherited target plugin. Note: this call needs to get
      * all the children of this page, and so its will resolve this page. might
      * be expensive.
      *
      * @return Returns all the inherited styles of this page. Returns an empty
      * hashtable if page is not expandable, does not have any includes,
      * or has includes that do not merge styles.
      */
     public Hashtable getAltStyles() {
         // call get children first to resolve includes and populate hashtable.
 // Resolving children will initialize the style vectors.
 getChildren();
         return altStyles;
     }

     /**
      * Adds the given style to the list. Style is not added if it already exists
      * in the list.
      *
      * @param style
      */
     protected void addStyle(String style) {
         if (!initStyles(style))
             return;
         if (styles.contains(style))
             return;
         styles.add(style);
     }

     public void insertStyle(String style, int location) {
         if (!initStyles(style))
             return;
         if (styles.contains(style))
             return;
         styles.add(location, style);
     }



     /**
      * Adds the given style to the list.Style is not added if it already exists
      * in the list.
      *
      * @param altStyle
      */
     protected void addAltStyle(String altStyle, Bundle bundle) {
         if (!initAltStyles(altStyle))
             return;
         if (altStyles.containsKey(altStyle))
             return;
         altStyles.put(altStyle, bundle);
     }


     /**
      * Util method to add given styles to the list.
      *
      */
     protected void addStyles(String [] styles) {
         if (styles == null)
             return;
         for (int i = 0; i < styles.length; i++)
             addStyle(styles[i]);
     }

     /**
      * Util method to add map of altstyles to list.
      */
     protected void addAltStyles(Hashtable altStyles) {
         if (altStyles == null)
             return;
         if (this.altStyles == null)
             // delay creation until needed.
 this.altStyles = new Hashtable ();
         this.altStyles.putAll(altStyles);
     }


     private boolean initStyles(String style) {
         if (style == null)
             return false;
         if (this.styles == null)
             // delay creation until needed.
 this.styles = new Vector ();
         return true;
     }

     private boolean initAltStyles(String style) {
         if (style == null)
             return false;
         if (this.altStyles == null)
             // delay creation until needed.
 this.altStyles = new Hashtable ();
         return true;
     }


     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType()
      */
     public int getType() {
         return AbstractIntroElement.ABSTRACT_PAGE;
     }

     /*
      * Override parent behavior to lazily initialize styles vectors. This will
      * only be called once, if resolved == false. In the case of DOM model,
      * resolve this page's full DOM.
      *
      * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroContainer#resolveChildren()
      */
     protected void resolveChildren() {
         // flag would be set
 if (isXHTMLPage)
             resolvePage();
         else
             super.resolveChildren();
     }



     /**
      * Override parent behavior to add support for HEAD & Title element in pages
      * only, and not in divs.
      *
      * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroContainer#getModelChild(org.eclipse.core.runtime.IConfigurationElement)
      */
     protected AbstractIntroElement getModelChild(Element childElement,
             Bundle bundle, String base) {
         AbstractIntroElement child = null;
         if (childElement.getNodeName().equalsIgnoreCase(IntroHead.TAG_HEAD)) {
             child = new IntroHead(childElement, bundle, base);
         } else if (childElement.getNodeName().equalsIgnoreCase(
             IntroPageTitle.TAG_TITLE)) {
             // if we have a title, only add it as a child if we did not load one
 // before. A page can only have one title.
 if (title == null) {
                 child = new IntroPageTitle(childElement, bundle);
             }
         }
         if (child != null)
             return child;
         return super.getModelChild(childElement, bundle, base);
     }

     /**
      * Returns all head contributions in this page. There can be more than one
      * head contribution in the page;
      *
      * @return
      */
     public IntroHead[] getHTMLHeads() {
         return (IntroHead[]) getChildrenOfType(AbstractIntroElement.HEAD);
     }


     /**
      * load the children of this container. Override parent behavior because we
      * want to support loading content from other xml files. The design is that
      * only the id and content from the existing page are honored. All other
      * attributes are what they are defined in the external paget. For XHTML
      * content, all info is in the xhtml page. If we fail to load the page,
      * display the Invalid Page page.
      */
     protected void loadChildren() {
         if (content == null) {
             // no content. do regular loading.
 super.loadChildren();
             return;
         }

         // content attribute is defined. It either points to an XHTML file, or
 // an introContent.xml file. Process each case. Assume it is an
 // introContent file.
 // INTRO: XHTML file is loaded needlessly when we have XHTML content and
 // SWT presentation.
 IntroContentParser parser = new IntroContentParser(content);
         Document dom = parser.getDocument();
         if (dom == null) {
             // bad xml. This could be bad XHTML or bad intro XML. Parser would
 // have logged fact. Load dom for invalid page, and make sure to
 // force an extract on parent folder to enabling jarring.
 Bundle introBundle = Platform.getBundle(IIntroConstants.PLUGIN_ID);
             ModelUtil.ensureFileURLsExist(introBundle, INVALID_CONTENT);

             String invalidContentFilePath = BundleUtil
                 .getResolvedResourceLocation(INVALID_CONTENT, introBundle);
             parser = new IntroContentParser(invalidContentFilePath);
             dom = parser.getDocument();
             // make sure to override all attributes to resolve the Invalid
 // Page page correctly.
 content = invalidContentFilePath;
             this.base = INVALID_CONTENT_BASE;
             setBundle(introBundle);
         }

         // parse content depending on type. Make sure to set the loaded flag
 // accordingly, otherwise content file will always be parsed.
 if (parser.hasXHTMLContent()) {
             loadXHTMLContent(dom);
             // make sure to use correct base.
 init(element, getBundle(), initialBase);
             super.loadChildren();
         } else
             // load the first page with correct id, from content xml file.
 loadXMLContent(dom);
     }

     /**
      * Load the xml content from the introContent.xml file pointed to by the
      * content attribute, and loaded into the passed DOM. Load the first page
      * with correct id from this content file.
      *
      * @param dom
      */
     private void loadXMLContent(Document dom) {
         Element[] pages = ModelUtil.getElementsByTagName(dom,
             AbstractIntroPage.TAG_PAGE);
         if (pages.length == 0) {
             Log.warning("Content file has no pages."); //$NON-NLS-1$
 return;
         }
         // point the element of this page to the new element. Pick first page
 // with matching id. Make sure to disable loading of children of current
 // element if a matching page in the external content file is not found.
 boolean foundMatchingPage = false;
         for (int i = 0; i < pages.length; i++) {
             Element pageElement = pages[i];
             if (pageElement.getAttribute(AbstractIntroIdElement.ATT_ID).equals(
                 getId())) {
                 this.element = pageElement;
                 // call init on the new element. the filtering and the style-id
 // are loaded by the parent class.
 init(pageElement, getBundle(), base);
                 // INTRO: revisit. Special processing here should be made more
 // general. we know id is correct.
 style_id = getAttribute(element,
                     AbstractBaseIntroElement.ATT_STYLE_ID);
                 filteredFrom = getAttribute(element,
                     AbstractBaseIntroElement.ATT_FILTERED_FROM);
                 sharedStyle = getAttribute(element, ATT_SHARED_STYLE);
                 if (sharedStyle == null)
                     sharedStyle = "true"; //$NON-NLS-1$
 foundMatchingPage = true;
             }
         }
         if (foundMatchingPage)
             // now do children loading as usual.
 super.loadChildren();
         else {
             // page was not found in content file. Perform load actions, and log
 // fact. init the children vector.
 children = new Vector ();
             loaded = true;
             // free DOM model for memory performance.
 element = null;
             Log.warning("Content file does not have page with id= " + getId()); //$NON-NLS-1$
 }
     }

     private void loadXHTMLContent(Document dom) {
         // no need to load any children since we use XSLT to print XHTML
 // content. Simply cache DOM.
 this.dom = dom;
         this.isXHTMLPage = true;
         // init empty children vector.
 children = new Vector ();
         loaded = true;
     }

     /**
      * Returns the DOM representing an external XHTML file. May return null if
      * extension content is 3.0 format. The page is resolved before returning,
      * meaning includes are resolved, and the base of the page is defined.
      *
      * @return
      */
     public Document getResolvedDocument() {
         // we need to force a getChildren to resolve the page.
 getChildren();
         return dom;
     }


     /**
      * Returns the DOM representing an external XHTML file. May return null if
      * extension content is 3.0 format. The page is NOT resolved before
      * returning. It is retruned as given by the dom parser.
      *
      * @return
      */
     public Document getDocument() {
         // we only need to load children here.
 if (!loaded)
             loadChildren();
         return dom;
     }


     /**
      * Returns whether or not we have an XHTML page as the content for this
      * page. The XHTML page is defined through the content attribute. This
      * method forces the content file to be parsed and loaded in memory.
      *
      * @return
      */
     public boolean isXHTMLPage() {
         // we need to force loading of children since we need to determine
 // content type. Load the children without resolving (for performance),
 // this will set the XHTML flag at the page level.
 if (!loaded)
             loadChildren();
         return isXHTMLPage;
     }


     /**
      * Deep searches all children in this container's DOM for the first child
      * with the given id. The element retrieved must have the passed local name.
      * Note that in an XHTML file (aka DOM) elements should have a unique id
      * within the scope of a document. We use local name because this allows for
      * finding intro anchors, includes and dynamic content element regardless of
      * whether or not an xmlns was used in the xml. note: could not have used
      * inheritance from parent container because return type for parent is intro
      * legacy model.
      *
      */
     public Element findDomChild(String id, String localElementName) {
         if (!loaded)
             loadChildren();
         // using getElementById is tricky and we need to have intro XHTML
 // modules to properly use this method.
 return ModelUtil.getElementById(dom, id, localElementName);
     }

     /**
      * Search for any element with the given id.
      *
      * @param id
      * @return
      */
     public Element findDomChild(String id) {
         return findDomChild(id, "*"); //$NON-NLS-1$

     }



     /**
      * Resolves the full page. It is called just before the page needs to be
      * displayed.
      * <li>adds a BASE child to the DOM HEAD element, if one is not defined.
      * All intro documents must have a base defined to resolve all urls.</li>
      * <li>resolves all includes in the page. This means importing target DOM.
      * </li>
      * <li>resolves all XHTML attributes for resources, eg: src, href
      * attributes.</li>
      */
     protected void resolvePage() {
         // insert base meta-tag,
 ModelUtil.insertBase(dom, ModelUtil.getParentFolderOSString(content));

         // resolve all relative resources relative to content file. Do it before
 // inserting shared style to enable comparing fully qualified styles.
 ModelUtil.updateResourceAttributes(dom.getDocumentElement(), this);

         // now add shared style.
 IntroModelRoot modelRoot = (IntroModelRoot)getParent();
         IntroPartPresentation presentation = modelRoot.getPresentation();
         String [] styles = presentation!=null?presentation.getImplementationStyles():null;
         if (styles != null && injectSharedStyle()) {
             for (int i=0; i<styles.length; i++)
                 ModelUtil.insertStyle(dom, styles[i]);
         }

         // filter the content
 if (domProcessor == null) {
             domProcessor = new DocumentProcessor(new ProcessorHandler[] { new FilterHandler(IntroEvaluationContext.getContext()) });
         }
         UAElement element = UAElementFactory.newElement(dom.getDocumentElement());
         domProcessor.process(element, null);
         
         // and resolve includes.
 resolveIncludes();

         // now remove all anchors from this page.
 ModelUtil.removeAllElements(dom, IntroAnchor.TAG_ANCHOR);
         resolved = true;
     }

     /**
      * Resolves all includes in this page. This means importing the DOM of the
      * target path into the current page DOM, and resolving XHTML attributes for
      * resources.
      */
     protected void resolveIncludes() {
         // get all includes elements in DOM.
 NodeList includes = dom.getElementsByTagNameNS("*", //$NON-NLS-1$
 IntroInclude.TAG_INCLUDE);
         // get the array version of the include nodelist to work around
 // replaceChild() DOM api design.
 Node [] nodes = ModelUtil.getArray(includes);
         for (int i = 0; i < nodes.length; i++) {
             Element includeElement = (Element) nodes[i];
             IntroInclude include = new IntroInclude(includeElement, getBundle());
             // result[0] is target parent page, result[1] is target element.
 Object [] results = findDOMIncludeTarget(include);
             Element targetElement = (Element) results[1];
             if (targetElement == null) {
                 String message = "Could not resolve following include: " //$NON-NLS-1$
 + ModelLoaderUtil.getLogString(getBundle(),
                             includeElement, IntroInclude.ATT_PATH);
                 Log.warning(message);
                 return;
             }

             // insert the target element instead of the include.
 Node targetNode = dom.importNode(targetElement, true);
             // update the src attribute of this node, if defined by w3
 // specs.
 AbstractIntroPage page = ((AbstractIntroPage) results[0]);
             ModelUtil.updateResourceAttributes((Element) targetNode, page);
             // use of replace API to remove include element is tricky. It
 // confuses the NodeList used in the loop above. Removing an include
 // removes it from the NodeList. Used cloned Array instead.
 includeElement.getParentNode().replaceChild(targetNode,
                 includeElement);
         }
     }


     /**
      * Find the target Element pointed to by the path in the include. It is
      * assumed that configId always points to an external config, and not the
      * same config of the inlcude.
      *
      * @param include
      * @param path
      * @return
      */
     private Object [] findDOMIncludeTarget(IntroInclude include) {
         String path = include.getPath();
         IntroModelRoot targetModelRoot = (IntroModelRoot) getParentPage()
             .getParent();
         String targetConfigID = include.getConfigId();
         if (targetConfigID != null)
             targetModelRoot = ExtensionPointManager.getInst().getModel(
                 targetConfigID);
         if (targetModelRoot == null)
             // if the target config was not found, skip this include.
 return null;
         return findDOMTarget(targetModelRoot, path);

     }



     /**
      * Finds the child element that corresponds to the given path in the passed
      * model.
      *
      * @param model
      * model containing target path.
      * @param path
      * the path to look for
      * @param results
      * two object array that will return the target intro page as the
      * first result, and the actual target DOM Element as the second
      * result. It is gauranteed to not be null. Content may be null.
      * @return target DOM element
      */
     public Object [] findDOMTarget(IntroModelRoot model, String path) {
         Object [] results = new Object [2];
         // path must be pageId/anchorID in the case of of XHTML pages.
 // pages.
 String [] pathSegments = StringUtil.split(path, "/"); //$NON-NLS-1$
 if (pathSegments.length != 2)
             // path does not have correct format. Return empty results.
 return results;

         // save to cast.
 AbstractIntroPage targetPage = (AbstractIntroPage) model.findChild(
             pathSegments[0], ABSTRACT_PAGE);

         if (targetPage != null) {
             results[0] = targetPage;
             Element targetElement = targetPage.findDomChild(pathSegments[1]);
             if (targetElement != null)
                 results[1] = targetElement;
         }
         return results;
     }


     /**
      * @return Returns the content.
      */
     public String getContent() {
         return content;
     }



     /**
      * Deep copy since class has mutable objects.
      */
     public Object clone() throws CloneNotSupportedException {
         AbstractIntroPage clone = (AbstractIntroPage) super.clone();
         if (title != null) {
             IntroPageTitle clonedTitle = (IntroPageTitle) title.clone();
             clonedTitle.setParent(clone);
             clone.title = clonedTitle;
         }
         // styles are safe for a shallow copy.
 if (styles != null)
             clone.styles = (Vector ) styles.clone();
         if (altStyles != null)
             clone.altStyles = (Hashtable ) altStyles.clone();
         return clone;
     }

     /**
      * Used when cloning pages to assign a unique id. Cache original id before
      * setting.
      *
      * @param id
      */
     public void setId(String id) {
         this.originalId = this.id;
         this.id = id;
     }

     /*
      * Creates an IFrame and injects it as the only child of the specified path.
      */
     public boolean injectIFrame(String url, String embedTarget) {
         // embed url as IFrame in target div. We need to find target div in
 // this cloned page not in the original page.
 IntroGroup divToReplace = (IntroGroup) findTarget(embedTarget);
         if (divToReplace == null) {
             // we failed to find embed div, log and exit.
 Log.warning("Failed to find embedTarget: " + embedTarget //$NON-NLS-1$
 + " in page " + getId()); //$NON-NLS-1$
 return false;
         }

         this.iframe = new IntroInjectedIFrame(getElement(), getBundle());
         this.iframe.setParent(divToReplace);
         this.iframe.setIFrameURL(url);
         divToReplace.clearChildren();
         divToReplace.addChild(iframe);
         return true;
     }

     /**
      * Return true if this page is a cloned page that has an IFrame.
      *
      * @return
      */
     public boolean isIFramePage() {
         return (iframe != null) ? true : false;
     }


     public String getUnmangledId() {
         if (isIFramePage())
             return originalId;
         return id;
     }


     /**
      * Set the url of the embedded IFrame, if this page is an IFrame page.
      *
      * @param url
      */
     public void setIFrameURL(String url) {
         if (!isIFramePage())
             return;
         this.iframe.setIFrameURL(url);
     }

     /**
      * Return the url of the embedded IFrame, if this page is an IFrame page.
      *
      * @param url
      */
     public String getIFrameURL() {
         if (!isIFramePage())
             return null;
         return this.iframe.getIFrameURL();
     }

     /**
      * Returns the raw or unprocessed base location.
      */
     public String getInitialBase() {
         return initialBase;
     }
     
     /**
      * Return the url of the embedded IFrame, if this page is an IFrame page.
      *
      * @param url
      */
     public boolean injectSharedStyle() {
         return this.sharedStyle.equals("true") ? true : false; //$NON-NLS-1$
 }

 }

